package com.hsgene.common.util.web.mapping;

import com.hsgene.common.util.redis.RedisConstants;
import com.hsgene.common.util.resolver.IgnoreUserToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;

/**
 * 自定义RequestMappingHandlerMapping处理，处理被@IgnoreUserToken注解的Mapping，</br>
 * 缓存到Redis中，供网关拦截校验
 *
 * @author wangbing
 * @version 1.0, 2018/11/27
 */
public class HsgeneRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private final static Logger LOGGER = LoggerFactory.getLogger(HsgeneRequestMappingHandlerMapping.class);

    private final static String IGNORE_USER_TOKEN_URLS = "ignore_user_token_urls";

    @Value("#{'${spring.application.name}'.toLowerCase()}")
    private String springApplicationName;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    private String ignoreUrlsRedisKey;

    private List<Integer> platforms;

    public HsgeneRequestMappingHandlerMapping() {
    }

    public HsgeneRequestMappingHandlerMapping(List<Integer> platforms) {
        platforms.add(0);
        this.platforms = platforms;
    }

    @PostConstruct
    private void initIgnoreUrlsRedisKey() {
        // 示例：yinzigene:ms-mod-authority:ignore_user_token_urls
        this.ignoreUrlsRedisKey = formatIgnoreUrlsRedisKey(springApplicationName);
    }

    @Override
    @Nullable
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                info = typeInfo.combine(info);
            }

            // 此处针对BasicErrorController等特殊处理
            if (!CollectionUtils.isEmpty(info.getPatternsCondition().getPatterns())
                    && !CollectionUtils.isEmpty(info.getMethodsCondition().getMethods())) {
                // TODO 此处有待优化，根据源码可知一个Condition中的结果集存在多个，当前在处理时只处理一个的情况
                String uri = info.getPatternsCondition().getPatterns().toArray()[0].toString();
                String requestMethod = info.getMethodsCondition().getMethods().toArray()[0].toString();

                String pattern = requestMethod + " " +formatUri(uri);
                IgnoreUserToken ignoreUserToken = AnnotatedElementUtils.findMergedAnnotation(method, IgnoreUserToken.class);
                Boolean existed = redisTemplate.opsForSet().isMember(ignoreUrlsRedisKey, pattern);
                if (ignoreUserToken == null) {
                    if (Boolean.TRUE.equals(existed)) {
                        redisTemplate.opsForSet().remove(ignoreUrlsRedisKey, pattern);
                        platforms.parallelStream().forEach(e -> {
                            redisTemplate.opsForSet().remove(ignoreUrlsRedisKey + ":" + e, pattern);
                        });
                    }
                } else {
                    LOGGER.info("IgnoreUserToken: uri = " + uri + "\t" + "method = " + requestMethod);
                    if (!Boolean.TRUE.equals(existed)) {
                        redisTemplate.opsForSet().add(ignoreUrlsRedisKey, pattern);
                    }
                    int[] ignorePlatforms = ignoreUserToken.platform();
                    platforms.forEach(e -> {
                        Boolean ignored = redisTemplate.opsForSet().isMember(ignoreUrlsRedisKey + ":" + e, pattern);
                        boolean needIgnore = false;
                        for (int ignorePlatform : ignorePlatforms) {
                            if (e == ignorePlatform) {
                                needIgnore = true;
                                break;
                            }
                        }
                        if (needIgnore) {
                            if (!Boolean.TRUE.equals(ignored)) {
                                redisTemplate.opsForSet().add(ignoreUrlsRedisKey + ":" + e, pattern);
                            }
                        } else {
                            if (Boolean.TRUE.equals(ignored)) {
                                redisTemplate.opsForSet().remove(ignoreUrlsRedisKey + ":" + e, pattern);
                            }
                        }
                    });

                }
            }

        }
        return super.getMappingForMethod(method, handlerType);
    }

    @Nullable
    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        RequestCondition<?> condition = (element instanceof Class ?
                getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
        return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
    }

    public static String formatUri(String uri) {
        int index = uri.lastIndexOf("}");
        if (index == -1) {
            return uri;
        }
        int index2 = uri.lastIndexOf("{");
        if (index2 == -1) {
            return uri;
        }

        return formatUri(uri.replace(uri.substring(index2, index + 1), ".+"));
    }

    public static String formatIgnoreUrlsRedisKey(String applicationName) {
        return new StringBuilder(RedisConstants.DEFAULT_REDIS_PREFIX)
                .append(RedisConstants.COLON_SPLIT)
                .append(applicationName)
                .append(RedisConstants.COLON_SPLIT)
                .append(IGNORE_USER_TOKEN_URLS).toString();
    }
}
